"""a collection of model-related helper classes and functions"""

# Import standard Python libraries for handling dates, JSON, logging, and regular expressions.
import datetime
import json
import logging
import re

# Import third-party libraries.
# humanize is used for creating human-readable time deltas (e.g., "5 minutes ago").
import humanize
# sqlalchemy is the ORM used for database interactions.
import sqlalchemy as sa
# Import specific components from Flask-AppBuilder for model rendering and auditing.
from flask_appbuilder.models.decorators import renders
from flask_appbuilder.models.mixins import AuditMixin
# Import utilities from SQLAlchemy for declarative class definitions and relationships.
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship

# Import custom user model from the local application.
from myapp.models.model_user import MyUser
# Import Flask's global object 'g' to access request-specific data.
from flask import g


# This function extends a model query to automatically filter by the current region.
def query_with_region(model, **filters):
    """
    Adds a 'region' filter to a SQLAlchemy query based on the global context.
    :param model: The SQLAlchemy model class to query.
    :param filters: Additional keyword arguments to use for filtering.
    :return: A SQLAlchemy query object with the region filter applied.
    """
    # Add the current region from Flask's global 'g' object to the filter criteria.
    filters['region'] = g.region.key
    # Create a query on the specified model, applying all provided filters.
    query = model.query.filter_by(**filters)
    # Return the constructed query object.
    return query

# This function safely parses a JSON string into a Python dictionary.
def json_to_dict(json_str):
    """
    Parses a JSON string into a dictionary, handling potential formatting issues.
    This function is more lenient than the standard json.loads by removing trailing commas.
    :param json_str: The JSON string to parse.
    :return: A dictionary representation of the JSON, or an empty dictionary if input is empty or invalid.
    """
    # Check if the input string is not None or empty.
    if json_str:
        # Use regex to remove trailing commas before a closing brace '}' to prevent parsing errors.
        val = re.sub(',[ \t\r\n]+}', '}', json_str)
        # Use regex to remove trailing commas before a closing bracket ']' to prevent parsing errors.
        val = re.sub(',[ \t\r\n]+\\]', ']', val)
        # Parse the cleaned string into a Python dictionary.
        return json.loads(val)
    else:
        # Return an empty dictionary if the input string is empty.
        return {}


# This class is a modified version of Flask-AppBuilder's AuditMixin.
# @pysnooper.snoop()
class AuditMixinNullable(AuditMixin):
    """Altering the AuditMixin to use nullable fields

    Allows creating objects programmatically outside of CRUD
    """

    # Override 'created_on' to be nullable, allowing records to be created without this field being set.
    created_on = sa.Column(sa.DateTime, default=datetime.datetime.now, nullable=True)
    # Override 'changed_on' to be nullable and to automatically update on modification.
    changed_on = sa.Column(
        sa.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, nullable=True
    )

    # Use @declared_attr to define the 'created_by' relationship at the class level.
    @declared_attr
    def created_by(cls):
        # Define a many-to-one relationship with the MyUser model for the creator.
        return relationship(
            MyUser,
            # The join condition is explicitly defined to link this model's foreign key to the user's ID.
            primaryjoin='%s.created_by_fk == MyUser.id' % cls.__name__,
            # Disable type checks to prevent potential circular dependency issues.
            enable_typechecks=False,
        )

    # Use @declared_attr to define the 'changed_by' relationship.
    @declared_attr
    def changed_by(cls):
        # Define a many-to-one relationship with the MyUser model for the last modifier.
        return relationship(
            MyUser,
            primaryjoin='%s.changed_by_fk == MyUser.id' % cls.__name__,
            enable_typechecks=False,
        )

    # 会生成数据库中的列
    # Use @declared_attr to define the foreign key column for the creator.
    @declared_attr
    def created_by_fk(self):  # noqa
        # This column stores the ID of the user who created the record.
        return sa.Column(
            sa.Integer,
            sa.ForeignKey('ab_user.id'), # Establishes a foreign key constraint to the 'ab_user' table.
            default=self.get_user_id, # Sets the default value to the current user's ID.
            nullable=True, # Allows this field to be null.
        )

    # Use @declared_attr to define the foreign key column for the last modifier.
    @declared_attr
    def changed_by_fk(self):  # noqa
        # This column stores the ID of the user who last changed the record.
        return sa.Column(
            sa.Integer,
            sa.ForeignKey('ab_user.id'),
            default=self.get_user_id, # Sets the default value.
            onupdate=self.get_user_id, # Updates the value to the current user's ID on record update.
            nullable=True,
        )

    # This method is decorated with @renders to customize how the 'created_by' field is displayed in FAB views.
    @renders('created_by')
    def creator(self):  # noqa
        """
        Renders the 'created_by' user as a link in the UI.
        """
        # Call a helper method (likely from the parent mixin) to generate a user profile link.
        return self._user_link(self.created_by)
        # return self.created_by.username

    # This property provides a convenient way to get the linked username for the modifier.
    @property
    def changed_by_(self):
        """
        Returns a rendered link for the 'changed_by' user.
        """
        return self._user_link(self.changed_by)

    # This property returns a human-readable string for the 'changed_on' timestamp.
    @property
    def changed_on_humanized(self):
        """
        Formats the 'changed_on' timestamp into a relative time string (e.g., "3 days ago").
        """
        # Check if the 'changed_on' timestamp exists.
        if self.changed_on:
            # Use the humanize library to calculate the natural time difference.
            return humanize.naturaltime(datetime.datetime.now() - self.changed_on)
        else:
            # Return a default string if the timestamp is not set.
            return 'unknown'

    # 修改时间到目前的时间距离
    # This method renders the 'changed_on' field using the humanized format.
    @renders('changed_on')
    def modified(self):
        """
        Renders the 'changed_on' field in a human-readable format for FAB views.
        """
        # return Markup(f'<span class="no-wrap">{self.changed_on_humanized}</span>')
        return self.changed_on_humanized


# This mixin class adds a JSON column to a model for storing unstructured extra data.
class ExtraJSONMixin:
    """Mixin to add an `extra` column (JSON) and utility methods"""

    # Define a 'extra_json' column of type Text, defaulting to an empty JSON object string.
    extra_json = sa.Column(sa.Text, default='{}')

    # This property provides easy access to the 'extra_json' data as a Python dictionary.
    @property
    def extra(self):
        """
        Deserializes the 'extra_json' string into a dictionary.
        Returns an empty dictionary if parsing fails.
        """
        try:
            # Attempt to load the JSON string.
            return json.loads(self.extra_json)
        except Exception:
            # Return an empty dictionary if there's an error (e.g., invalid JSON).
            return {}

    # This method sets the entire 'extra_json' field from a dictionary.
    def set_extra_json(self, d):
        """
        Serializes a dictionary to a JSON string and updates the 'extra_json' column.
        :param d: The dictionary to store.
        """
        # Convert the dictionary to a JSON string and assign it to the column.
        self.extra_json = json.dumps(d)

    # This method updates a single key-value pair within the 'extra_json' data.
    def set_extra_json_key(self, key, value):
        """
        Sets a specific key-value pair within the 'extra' JSON data.
        :param key: The key to set.
        :param value: The value to associate with the key.
        """
        # Get the current 'extra' data as a dictionary.
        extra = self.extra
        # Set or update the specified key.
        extra[key] = value
        # Serialize the modified dictionary back to a JSON string.
        self.extra_json = json.dumps(extra)


# This function extracts a model path from a string, which can be either a plain path or a JSON object.
def parse_model_path(path):
    """

    Parses an input string to find a 'model_path'.
    The input can be a direct path string or a JSON string containing a 'model_path' key.
    :param path: The input string.
    :return: The extracted model path string, or None if not found.
    """
    # Return None immediately if the input path is None.
    if path is None:
        return None
    # Remove leading/trailing whitespace from the path string.
    path = path.strip()
    # Initialize the model_path variable.
    model_path = None
    # 判断是否为json
    # Check if the string looks like a JSON object.
    if path.startswith('{') and path.endswith(''}'):
        try:
            # Attempt to parse the string as JSON.
            path_dict = json.loads(path)
            # Get the value associated with the 'model_path' key.
            model_path = path_dict.get('model_path', None)
        except Exception as e:
            # Log any exceptions that occur during JSON parsing.
            logging.exception(e)
    else:
        # If it's not a JSON-like string, assume the entire string is the model path.
        model_path = path

    # If a model_path was successfully extracted, clean it up.
    if model_path is None:
        return None
    # Remove any leading/trailing whitespace from the final path.
    model_path = model_path.strip()

    # Return the cleaned model path.
    return model_path
